"""
@Project ：interface_platform
@File    ：execute_testcase_controller.py
@IDE     ：PyCharm
@Author  ：ZSR
@Date    ：2023/3/28 21:39
"""

import copy
import datetime
import json
import random
import string
import time
import traceback

import requests
from flask import request, copy_current_request_context
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, as_completed

from flask_login import login_required
from requests.adapters import HTTPAdapter

from backend import config
from backend.app import app, db
from flask_restful import Resource
from backend.models.test_project_model import TestProjectModel
from backend.models.test_report_model import TestReportModel
from backend.models.test_suite_model import TestSuiteModel
from backend.models.test_case_model import TestCaseModel
from backend.models.test_env_model import TestEnvModel
from backend.utils.code_utils import CodeUtil
from backend.utils.exception_utils import REQ_IS_EMPTY_ERROR, REQ_TYPE_ERROR, REQ_KEY_ERROR, REQ_VALUE_ERROR, \
    SUITE_ID_LIST_IS_NO_CASE
from backend.utils.make_response_utils import make_response
from flask import current_app

from backend.utils.replace_params import replace_params

session = requests.Session()  # 实例化session对象
session.mount("http://", HTTPAdapter(max_retries=3))  # 配置http请求的重试次数为3
session.mount("https://", HTTPAdapter(max_retries=3))  # 配置https请求的重试次数为3


def send_request(method, url, params, headers, body):
    """
    封装request请求
    :param method:  请求方法
    :param url: 请求地址
    :param params: 请求参数
    :param headers: 请求头
    :param body: 请求体
    :return: 响应体
    """
    response = None
    if method == "GET":
        response = session.get(url=url, params=params, headers=headers)
    if method == "POST":
        response = session.post(url=url, params=params, headers=headers, json=body)
    if method == "PUT":
        response = session.put(url=url, params=params, headers=headers, json=body)
    if method == "DELETE":
        response = session.delete(url=url, params=params, headers=headers, json=body)
    return response


# 生成测试报告id
def generate_ranom_str(randomlength=32):
    str_list = random.sample(string.digits + string.ascii_letters, randomlength - 13)
    random_str = ''.join(str_list)
    random_str = str(int(time.time() * 1000)) + random_str
    return random_str


def assert_testcase(predict, response):
    # 这里要写上断言代码，分别应该包括：正则表达式断言、JSON断言、断言时间等断言内容
    return True


def check_testcase(caseid_list):
    """
    校验caseId的测试套件、测试计划有没有被删除
    :param caseid_list: caseId列表
    :return: 通过校验的caseId列表
    """
    new_caseid_list = copy.deepcopy(caseid_list)
    for caseid in caseid_list:
        case_data = TestCaseModel.query.filter(TestCaseModel.id == caseid, TestCaseModel.isDeleted == 0).first()
        if not case_data:
            app.logger.info(f"caseid为{caseid}的用例在数据库当中查询不到，跳过这条用例")
            new_caseid_list.remove(caseid)  # 从要执行的用例编号列表中去除这条用例
            continue
        if not case_data.suite.isDeleted == '0':
            app.logger.info(f"caseid为{caseid}的用例的用例集{case_data.suite.id}已经被删除，跳过这条用例")
            new_caseid_list.remove(caseid)  # 从要执行的用例编号列表中去除这条用例
            continue
        if not case_data.suite.project.isDeleted == '0':
            app.logger.info(
                f"caseid为{caseid}的用例的用例集{case_data.suite.id}的测试计划{case_data.suite.project.id}已经被删除，跳过这条用例")
            new_caseid_list.remove(caseid)  # 从要执行的用例编号列表中去除这条用例
            continue
    app.logger.info(f"经过校验之后，要执行的测试用例列表为：{new_caseid_list}")
    return new_caseid_list


# 使用线程池执行用例的方法
def multi_thread(func, caseid_list=None):
    """
    :param func: 需要执行的方法
    :param caseid_list: 参数列表
    :return: 返回值
    """
    executor = ThreadPoolExecutor(config.NUMBERS)
    try:
        with executor as t:
            res = [t.submit(func, i).result() for i in caseid_list]
        return res
    except:
        app.logger.info(traceback.print_exc())


class ExecuteTestCaseController:
    env_id = None

    @classmethod
    def execute_testcase(cls, testcase_data, type=None):
        cls.env_id = testcase_data.get("env_id")
        start_time = datetime.datetime.now()  # 计算总时间
        new_caseid_list = []  # 存放校验后的测试case_id
        if type == "case_id_list":
            caseid_list = testcase_data.get("caseid_list")
            app.logger.info(f"要执行的测试用例编号列表为：{caseid_list}")
            # 校验caseid的测试套件、测试计划有没有被删除
            new_caseid_list = check_testcase(caseid_list)  # 调用检查测试用例的方法

        if type == "suite_id_list":
            suite_id_list = testcase_data.get("suite_id_list")
            for suite_id in suite_id_list:  # 遍历测试套件列表
                # 根据测试套件的编号，读取测试用例列表
                s_data = TestSuiteModel.query.filter(TestSuiteModel.id == suite_id,
                                                     TestSuiteModel.isDeleted == 0).first()
                if not s_data:
                    app.logger.info(f"suite_id：{suite_id} 中没有测试用例！")
                    raise SUITE_ID_LIST_IS_NO_CASE()
                app.logger.info(f"根据suite_id：{suite_id} 查找的要执行的测试套件：{s_data}")
                test_cases = s_data.test_case
                app.logger.info(f"从测试套件中反向查找的所有测试用例：{test_cases}")
                for test_case in test_cases:
                    if test_case.isDeleted == '0':
                        new_caseid_list.append(test_case.id)  # 将测试用例的id加入到caseid_list
        # 最终要执行的测试case列表
        app.logger.info(f"最终要执行的测试case列表：{new_caseid_list}")

        # 使用多线程执行测试用例
        app.logger.info("开始多线程执行测试用例：------------")

        # 执行测试用例的任务
        @copy_current_request_context
        def task_execute_testcase(caseid):
            start_time = datetime.datetime.now()  # 计算总时间
            # 查询数据库，把每一条caseid的数据记录都查询出来，然后才能拼接接口的数据，最后使用requests执行
            case_data = TestCaseModel.query.filter(TestCaseModel.id == caseid, TestCaseModel.isDeleted == '0').first()

            # 加载环境变量，把环境变量的所有变量，都提取出来，组成环境变量的字典
            env_id = ExecuteTestCaseController.env_id  # 使用ExecuteTestCaseController保存的env_id
            app.logger.info(f"要用的环境变量id为：{env_id}")
            env_data_model = TestEnvModel.query.filter_by(id=env_id, isDeleted=0).first()
            app.logger.info(f"从数据库当中查询出来的环境为：{env_data_model}")
            env_params_list = env_data_model.test_env.all()
            app.logger.info(f"反向查找出来的环境变量为：{env_params_list}")
            env_data = {}
            for params_model in env_params_list:  # 组合键值对形式
                env_data.update({params_model.params_key: params_model.params_value})
            app.logger.info(f"组合成键值对形式的环境变量为：{env_data}")
            # 使用环境变量中的变量值，替换用例中的变量（如果没有需要替换的变量，那么不做处理）
            case_data_elment_list = ['method', 'protocal', 'host', 'port', 'path', 'params', 'headers', 'body',
                                     'predict']
            for elment in case_data_elment_list:
                request_data = getattr(case_data, elment)  # 使用getattr获取case_data中的对应数据的值
                # 把value中的变量用环境变量进行替换
                replaced_data = replace_params(request_data, env_data)
                app.logger.info(f"当前替换的部分为:{elment} ， 替换前的值为: {request_data} ， 替换后的值为: {replaced_data}")
                # 把替换后的数据放回来
                setattr(case_data, elment, replaced_data)

            # 取出要测试的接口的请求方法、协议、域名、端口、查询参数、资源路径、请求头、请求体、预期结果
            method = case_data.method
            protocal = case_data.protocal
            host = case_data.host
            port = case_data.port
            path = case_data.path
            params = case_data.params
            headers = case_data.headers
            body = case_data.body
            predict = case_data.predict

            app.logger.info(f"替换后的case_data为：{case_data.to_dict()}")
            # 拼接接口的请求数据
            url = protocal + "://" + host + ":" + str(port) + path
            params = json.loads(params.replace('\'', '\"'))
            headers = json.loads(headers.replace('\'', '\"'))
            body = json.loads((body.replace('\'', '\"')))
            # 使用requests模块发送接口请求
            response = send_request(method, url, params, headers, body)

            end_time = datetime.datetime.now()  # 计算单条测试用例执行的时间
            time_delta = str((end_time - start_time).total_seconds())  # 单条用例的执行时间
            app.logger.info(f"用例编号{caseid}执行的结果为：{response.text}")

            # 断言结果
            assert_result = assert_testcase(predict, response)
            # 测试结果
            if assert_result:
                test_result = "通过"
            else:
                test_result = "不通过"
            # 每条测试用例测试报告的详细数据
            report_detail = {
                "case_id": case_data.id,
                "case_name": case_data.case_name,
                "suite_id": case_data.suite_id,
                "suite_name": case_data.suite.suite_name,
                "methods": case_data.method,
                "url": response.url,  # 本来应该拼接请求方法、域名、协议、端口、资源路径、查询参数，也可以从响应数据中获取URL
                "headers": case_data.headers,
                "body": case_data.body,
                "response_data": response.text,
                "status_code": response.status_code,
                "predict": case_data.predict,
                "assert_result": assert_result,
                "test_result": test_result,
                "overtime": time_delta,

            }

            return report_detail

        response_list = multi_thread(task_execute_testcase, caseid_list=new_caseid_list)

        app.logger.info("结束多线程执行测试用例：------------")
        try:
            app.logger.info(f"结束多线程执行测试用例的结果集为：{response_list}")
        except Exception as e:
            app.logger.info(f"请求出错：{e}")
        # 测试报告详情数据
        end_time = datetime.datetime.now()  # 计算总时间
        time_delta = str((end_time - start_time).total_seconds())
        reportName = reportId = generate_ranom_str()  # 生成测试报告的名称和ID
        testCount = len(response_list)  # 计算response_list的长度作为用例的数据
        passCount = 0
        failedCount = 0
        conclusion = "通过"
        for response_case_data in response_list:
            if response_case_data.get("assert_result"):
                passCount += 1
            else:
                failedCount += 1  # 包括了失败和错误
                conclusion = "不通过"
        passRate = passCount / testCount  # 通过率

        report_summerise = {
            "reportName": reportName,
            "reportId": reportId,
            "project_name": testcase_data.get("project_name"),
            "project_id": testcase_data.get("project_id"),
            "testCount": testCount,
            "passCount": passCount,
            "failedCount": failedCount,
            "passRate": passRate,
            "conclusion": conclusion,
            "total_time": time_delta,
            "comFrom": testcase_data.get("executionMode"),
            "create_time": datetime.datetime.now(),
            "case_data": str(response_list),
        }

        # 保存测试报告概要信息和详细信息到数据库
        testreport_data = TestReportModel(**report_summerise)
        db.session.add(testreport_data)
        db.session.commit()
        db.session.close()

        return response_list


class ExecuteTestCaseService(Resource):
    decorators = [login_required]
    def post(self):
        if not request.data:  # 校验有没有传请求数据
            raise REQ_IS_EMPTY_ERROR()
        if not request.is_json:  # 校验传的数据是不是json数据
            raise REQ_TYPE_ERROR()
        caseid_list = request.get_json().get("caseid_list")  # 读取测试用例编号列表
        suite_id_list = request.get_json().get("suite_id_list")  # 读取测试套件编号列表
        if not caseid_list and not suite_id_list:  # 如果用例和套件都没有传递，那么抛出keyerror
            raise REQ_KEY_ERROR()
        if caseid_list and suite_id_list:  # 如果用例和套件都传递了，那么抛出ValueError，因为只能选择一种去执行
            raise REQ_VALUE_ERROR()

        if suite_id_list:
            # 执行测试套件
            result = ExecuteTestCaseController.execute_testcase(request.get_json(), type="suite_id_list")
            app.logger.info(f"执行测试套件: {result}")
            return make_response(CodeUtil.SUCCESS, data=result)
        if caseid_list:
            # 执行测试用例
            result = ExecuteTestCaseController.execute_testcase(request.get_json(), type="case_id_list")
            app.logger.info(f"执行测试用例: {result}")
            return make_response(CodeUtil.SUCCESS, data=result)
